/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */


import weka.core.Attribute;
import weka.core.Instances;
import weka.core.Instance;
import weka.core.AttributeStats;
import weka.core.Utils;
import weka.core.NoSupportForMissingValuesException;

import java.util.ArrayList;
import java.io.Serializable;

/**
 *
 * @author mgomez
 */
public class TDIDTTree implements Serializable {

   /**
    * for serialization
    */
   static final long serialVersionUID = -163548171788845L;
   /**
    * Dato miembro para almacenar la raiz: null en caso de la raiz
    */
   private TDIDTTree parent;
   /**
    * Dato miembro para almacenar los hijos de un nodo
    */
   private ArrayList<TDIDTTree> sons;
   /**
    * Indica el atributo asociado al nodo
    */
   private Attribute attribute;
   /**
    * Referencia al atributo clase
    */
   private static Attribute classAttribute = null;
   /**
    * Clase asignada al nodo, si es termina
    */
   private int classValue;
   /**
    * Almacena en cada nodo la clase mayoritaria
    */
   private int majorityClass;
   /**
    * Almacena la distribucion de instancias de cada clase
    */
   private AttributeStats classDistribution = null;
   /**
    * Dato miembro para indicar si un nodo es terminal
    */
   private boolean leaf;
   /**
    * Dato miembro para indicar si el nodo corresponde a una rama perdida
    */
   private boolean lost;
   /**
    * The header information (for printing the tree).
    */
   protected static Instances info = null;

   /**
    * Constructor por defecto
    */
   public TDIDTTree() {
      leaf = false;
   }

   /**
    * Permite acceder al dato miembro parent
    *
    * @return parent
    */
   public TDIDTTree getParent() {
      return parent;
   }

   /**
    * Permite acceder al dato miembro attribute
    *
    * @return attribute
    */
   public Attribute getAttribute() {
      return attribute;
   }

   /**
    * Construye el arbol a partir de un conjunto de instancias
    *
    * @param data the training data
    * @exception Exception if decision tree can't be built successfully
    */
   public void makeTree(Instances data, Criteria criteria) throws Exception {
      TDIDTTree son;

      // Se comprueba si se ha asignado ya la variable clase
      if (classAttribute == null) {
         classAttribute = data.classAttribute();
      }

      // Se obtienen los atributos del problema mediante el metodo
      // de utilidad, pasando como argumento este nodo en desarrollo
      // (this)
      ArrayList<Attribute> unused = TDIDTUtils.getUnusedAttributes(data, this);
      System.out.println("Atributos no usados........................");
      for (int i = 0; i < unused.size(); i++) {
         System.out.println(unused.get(i).name());
      }

      // Se selecciona de forma aleatoria el siguiente atributo. En el 
      // caso generico habria que comprobar que quedan atributos por
      // usar
      attribute = criteria.getNext(data, unused);

      // Se almacena el reparto de instancias por clase
      classDistribution = data.attributeStats(classAttribute.index());

      System.out.println("Se selecciona el atributo: " + attribute.name());

      // Hay que particionar los datos segun su valor: en cada rama
      // se finaliza el proceso asignando la clase mayoritaria. EN
      // cada rama se asignan las instancias asociadas a cada uno de
      // los valores
      // Se crea un conjunto de datos para cada valor, gracias al
      // metodo split, en TDIDTUtils
      Instances[] splitData = TDIDTUtils.splitData(data, attribute);

      // Se crea el array list de hijos
      sons = new ArrayList<TDIDTTree>();

      // Se consideran los valores del atributo
      for (int i = 0; i < attribute.numValues(); i++) {
         // Se crea nuevo nodo para el valor i
         son = new TDIDTTree();

         // Su padre sera este nodo
         son.parent = this;

         // A todos los hijos se les asigna null como variable
         // attribute. Esto indica que se trata de una hija
         son.attribute = null;

         // Se indica que es hoja
         son.leaf = true;

         // Se almacenan las estadisticas asociadas a cada nodo, en
         // relacion con la variable clase
         son.classDistribution = splitData[i].attributeStats(classAttribute.index());

         // Se almacena la clase mayoritaria
         son.majorityClass = Utils.maxIndex(son.classDistribution.nominalCounts);

         // Se almacena el valor de la variable clase
         son.classValue = son.majorityClass;

         // Se asigna el hijo al conjunto de hijos
         sons.add(son);
      }
   }

   /**
    * Clasifica una instancia siguiendo el arbol
    *
    * @param instance instancia a clasificar
    * @return clasificacion propuesta
    * @throws NoSupportForMissingValuesException excepcion a generar si hay
    * valores perdidos
    */
   public double classifyInstance(Instance instance)
           throws NoSupportForMissingValuesException {

      // Si hay valores perdidos ha que generar la 
      // correspondiente excepcion
      if (instance.hasMissingValue()) {
         throw new NoSupportForMissingValuesException(
                 "TDIDT: no missing values, "
                 + "please.");
      }

      // Si es un nodo hoja, devuelve el valor de
      // classValue, la clase propuesta para todas
      // las instancias que correspondan con el
      // nodo
      if (leaf) {
         return classValue;
      } else {
         // Si no se trata de un nodo terminal hay que 
         // seguir descendiendo por él hasta llegar a 
         // un terminal. Para seguir hacia abajo se 
         // continua la busqueda sobre el hijo que se
         // corresponde con el valor que en la instancia
         // toma la variable atributo representada por 
         // el nodo
         double attributeValue = instance.value(attribute);
         return sons.get((int) attributeValue).classifyInstance(instance);
      }
   }

   /**
    * Muestra la informacion de texto sobre un determinado arbol, a partir de
    * cierto nivel
    *
    * @param nivel a partir del que se parte
    * @return cadena de texto
    */
   public String toString(int level) {

      StringBuffer text = new StringBuffer();

      if (leaf) {
         if (Double.isNaN(classValue)) {
            text.append(": null");
         } else {
            text.append("  ->  " + classAttribute.name() + " : " + classAttribute.value(classValue));
         }
      } else {
         for (int j = 0; j < attribute.numValues(); j++) {
            text.append("\n");
            for (int i = 0; i < level; i++) {
               text.append("|  ");
            }
            text.append(attribute.name() + " = " + attribute.value(j));
            if (sons != null && sons.get(j) != null) {
               text.append(sons.get(j).toString(level + 1));
            }
         }
      }
      return text.toString();
   }

   /**
    * Devuelve el numero de nodos del arbol
    *
    * @return numero de nodos
    */
   public double measureTreeSize() {
      // Llamada al metodo que calcula el numero de nodos
      return numNodes();
   }

   /**
    * Devuelve el numero de hojas del arbol
    *
    * @return numero de hojas
    */
   public double measureNumLeaves() {
      return numLeaves();
   }

   /**
    * Devuelve el numero de reglas (igual que el de hojas)
    *
    * @return numero de hojas - reglas
    */
   public double measureNumRules() {
      return numLeaves();
   }

   /**
    * Devuelve el numero de hojas del arbol
    *
    * @return the numero de hojas
    */
   private int numLeaves() {
      int num = 0;
      int i;

      // Si es hoja, devuelve 1
      if (leaf) {
         return 1;
      } else {
         // Suma los correspondientes a cada hijo
         for (i = 0; i < sons.size(); i++) {
            num = num + sons.get(i).numLeaves();
         }
      }

      return num;
   }

   /**
    * Devuelve el numero de nodos
    *
    * @return numero de nodos
    */
   private int numNodes() {
      int no = 1;
      int i;

      // Si no es hoja
      if (!leaf) {
         // Considera cada hijo
         for (i = 0; i < sons.size(); i++) {
            no = no + sons.get(i).numNodes();
         }
      }

      // Devuelve el contador
      return no;
   }

   /**
    * Muestra informacion sobre los nodos del arbol
    *
    * @param text texto a agregar a la descripcion
    * @param num identificador del nodo
    * @param parent padre de los nodos a describir
    * @return identificador del siguiente nodo
    * @throws Exception si hubo algun problema
    */
   public int toGraph(StringBuffer text, int num, TDIDTTree parent) throws Exception {
      // Se incrementa el contador de nodos
      num++;
      
      // Si es hoja, muestra su informacion (mediante el metodo
      // leafString, y no hace falta generar nuevas llamadas
      if (leaf == true) {
         text.append("N" + Integer.toHexString(TDIDTTree.this.hashCode())
                 + " [label=\"" + num + leafString() + "\""
                 + " shape=box]\n");

      } 
      else {
         // Si no es hoja, se agrega el nombre del nodo
         text.append("N" + Integer.toHexString(TDIDTTree.this.hashCode())
                 + " [label=\"" + num + ": "
                 + attribute.name() + "\"]\n");

         // Se continua con la descripcion de cada hijo
         for (int i = 0; i < sons.size(); i++) {
            text.append("N"
                    + Integer.toHexString(TDIDTTree.this.hashCode())
                    + "->" + "N"
                    + Integer.toHexString(sons.get(i).hashCode())
                    + " [label=\"");
            text.append(" = " + attribute.value(i));
            text.append("\"]\n");
            num = sons.get(i).toGraph(text, num, this);
         }
      }

      // Se devuelve el identificador del siguiente nodo
      return num++;
   }

   /**
    * Muestra la informacion de un nodo hoja
    *
    * @return texto describiendo al nodo
    * @throws Exception si algo falla
    */
   protected String leafString() throws Exception {
      double sum = 0, maxCount = 0;
      int maxIndex = 0;
      
      // Si hay datos que mostrar (no es un nodo perdido),
      // se muestra
      if (classDistribution != null) {
         // Se determina cuantas instancias llegaron al nodo
         sum = Utils.sum(classDistribution.nominalCounts);
         
         // Se obtiene la clase mayoritaria y cuantas instancias
         // estan asociadas a ella
         maxIndex = Utils.maxIndex(classDistribution.nominalCounts);
         maxCount = classDistribution.nominalCounts[maxIndex];
      }
      
      // Se devuelve informacion sobre clase mayoriaria, numero
      // de instancias, instancias correspondientes a la clase
      // mayoria e instancias asignadas a otras clases
      return " : "
              + classAttribute.value(maxIndex)
              + " ("
              + Utils.doubleToString(sum, 2)
              + " - "
              + Utils.doubleToString(maxCount, 2)
              + "/"
              + Utils.doubleToString(sum - maxCount, 2) + ")";
   }
}
